//--------------------------------------------------------------------------
// 
//  Copyright (c) Chili Software.  All rights reserved. 
// 
//  File: TimestampConcurrencyManager.cs
//
//  Description: Implements a time stamp concurrency mechanism.
// 
//--------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using Chili.Opf3.Query;
using Chili.Opf3.Mapping;

namespace Chili.Opf3.Concurrency.TimeStamp
{
	/// <summary>
	/// Implements the time stamp concurrency manager. This class uses a time stamp check
	/// to discover if somebody else has changed the object in the meantime.
	/// </summary>
	/// <remarks>
	/// This class implements a time stamp check that is used to validate an object against the storage.
	/// It checks if somebody else changed the object in the meantime (while it has been in memory)
	/// and informs the <see cref="Opf3.ObjectContext">ObjectContext</see> whether a problem has
	/// been encountered.
	/// </remarks>
	/// <example>
	/// The following example shows the usage of the class in your application.
	/// <code lang="CS">
	/// [Persistent("USER")]
	/// public class User
	/// {
	///     // ... Other code of the class.
	///
	///     [TimeStamp]
	///		[Field("TIME_STAMP")]
	///     public string TimeStamp
	///     {
	///         // ... code of the property.
	///     }
	/// }
	///
	/// // ... Other code
	/// 
	/// // Creates a new ObjectContext that uses an MsSql Server as storage.
	/// ObjectContext context = new ObjectContext(new MsSqlStorage("sa", "", "localhost", "application"));
	/// // Set the time stamp concurrency manager. The ObjectContext uses it from 
	/// // now on to check the concurrency.
	/// context.ConcurrencyManager = new TimeStampConcurrencyManager();
	/// 
	/// // ... Other code
	/// </code>
	/// </example>
	public sealed class TimeStampConcurrencyManager : IConcurrencyManager
	{
		private static Dictionary<Type, IMemberInfo> _memberInfos;

		#region IConcurrencyManager Members

		/// <summary>
		/// Invoked before the update operation of the object.
		/// </summary>
		/// <param name="obj">Persistent object that is validated.</param>
		/// <param name="e">EventArgs of the current routine.</param>
		/// <exception cref="Opf3.Concurrency.ConcurrencyException">A concurrency problem has been encountered.</exception>
		public void OnBeforeUpdate<T>(T obj, ConcurrencyManagerEventArgs e)
		{
			// Get the value of the member holding the time stamp.
			IMemberInfo info = GetTimeStampMember(obj);
			object value = info.GetValue(obj);

			// Reload the object from the storage.
			using (ObjectReader<T> reader = e.Context.GetObjectReader<T>(ObjectExpression.CreateLoadExpression(obj)))
			{
				// Somebody else deleted the object. Throw a concurrency exception.
				if (!reader.Read())
					throw new ConcurrencyException("A concurrency exception happened.", obj, null);

				// Get the value of the member holding the time stamp.
				object reloadedValue = GetTimeStampMember(reader.Current).GetValue(reader.Current);

				// Validate the results and throw an exception if required.
				if ((reloadedValue == null && value != null) || (reloadedValue != null && value == null))
					throw new ConcurrencyException("A concurrency exception happened.", obj, reader.Current);
				if (reloadedValue != null && value != null && reloadedValue.ToString() != value.ToString())
					throw new ConcurrencyException("A concurrency exception happened.", obj, reader.Current);
			}

			// Set a new guid as value of the time stamp member.
			info.SetValue(obj, Guid.NewGuid().ToString());
		}

		/// <summary>
		/// Invoked after the update operation of the object.
		/// </summary>
		/// <param name="obj">Persistent object that is validated.</param>
		/// <param name="e">EventArgs of the current routine.</param>
		public void OnAfterUpdate<T>(T obj, ConcurrencyManagerEventArgs e)
		{
			// Here is nothing to do.
		}

		/// <summary>
		/// Invoked before the insert operation of the object.
		/// </summary>
		/// <param name="obj">Persistent object that is validated.</param>
		/// <param name="e">EventArgs of the current routine.</param>
		public void OnBeforeInsert<T>(T obj, ConcurrencyManagerEventArgs e)
		{
			IMemberInfo info = GetTimeStampMember(obj);
			// Set a new guid as value of the time stamp member.
			info.SetValue(obj, Guid.NewGuid().ToString());
		}

		/// <summary>
		/// Invoked after the insert operation of the object.
		/// </summary>
		/// <param name="obj">Persistent object that is validated.</param>
		/// <param name="e">EventArgs of the current routine.</param>
		public void OnAfterInsert<T>(T obj, ConcurrencyManagerEventArgs e)
		{
			// Here is nothing to do.
		}

		/// <summary>
		/// Invoked before the delete operation of the object.
		/// </summary>
		/// <param name="obj">Persistent object that is validated.</param>
		/// <param name="e">EventArgs of the current routine.</param>
		public void OnBeforeDelete<T>(T obj, ConcurrencyManagerEventArgs e)
		{
			// Here is nothing to do.
		}

		/// <summary>
		/// Invoked after the load operation of the object.
		/// </summary>
		/// <param name="obj">Persistent object that is validated.</param>
		/// <param name="e">EventArgs of the current routine.</param>
		public void OnAfterLoad<T>(T obj, ConcurrencyManagerEventArgs e)
		{
			// Here is nothing to do.
		}

		#endregion

		#region GetTimeStampMember

		/// <summary>
		/// Returns the member of the object that holds the time stamp.
		/// </summary>
		/// <param name="obj">The object that is searched for the time stamp.</param>
		private IMemberInfo GetTimeStampMember(object obj)
		{
			Type type = obj.GetType();

			IMemberInfo m = GetMemberInfoFromDictionary(type);
			if (m != null)
				return m;
			else
			{
				TypeMapping typeMapping = TypeMapping.GetTypeMapping(type);

				// Loop over all member in the type mapping.
				foreach (IMemberInfo info in typeMapping.Members)
				{
					// Loop over all custom attributes of the member.
					foreach (object attribute in info.GetCustomAttributes(true))
					{
						TimeStampAttribute a = attribute as TimeStampAttribute;
						if (a != null)
						{
							// Only string is supported as member type.
							if (info.MemberType != typeof(string))
								throw new TimeStampTypeInvalidException("Only string is supported for the member holding the TimeStampAttribute.");

							// Add the items to the dictionaries.
							AddMemberInfo(type, info);
							return info;
						}
					}
				}
			}

			// Throw an exception if we get here.
			throw new TimeStampNotFoundException("The persistent object has no member with the TimeStampAttribute set.");
		}

		#endregion

		#region Utility Methods

		/// <summary>
		/// This routine adds the meta info object for the given type to the dictionary.
		/// </summary>
		/// <param name="type">The type of the object for that the meta info object is added.</param>
		/// <param name="memberInfo">The member info object of the current member.</param>
		private void AddMemberInfo(Type type, IMemberInfo memberInfo)
		{
			lock (this)
			{
				if (_memberInfos == null)
					_memberInfos = new Dictionary<Type, IMemberInfo>();
				if (!_memberInfos.ContainsKey(type))
					_memberInfos.Add(type, memberInfo);
			}
		}

		/// <summary>
		/// Gets the meta info object for the given type from the dictionary.
		/// </summary>
		/// <param name="type">The type for that the meta info object is searched.</param>
		private IMemberInfo GetMemberInfoFromDictionary(Type type)
		{
			lock (this)
			{
				if (_memberInfos == null || !_memberInfos.ContainsKey(type))
					return null;
				return _memberInfos[type];
			}
		}

		#endregion
	}
}
